Testing Clean Cleaner Cleanup

In Replacing Finalizers with Cleaners, the cleanup of resources encapsulated by heap objects is arranged using two complementary mechanisms, try-with-resources, and the Cleaner. Resources needing some kind of cleanup include security sensitive data and off-heap resources such as file descriptors or native handles. The cleanup should always occur and should occur as soon as possible when the resource is no longer active.

One of the questions that arises as Cleaners replace finalizers is how to test that the cleanup is working. For try-with-resources the test is pretty straight-forward, the state we need to verify is in an object that is or can be made accessible to the test and it is clear when to check that’s its done (after the close).

  1. In a try-with-resources statement, create a reference to the object
  2. Extract a reference to the encapsulated state data that is to be cleaned
  3. Exit the try-with-resources statement, implicitly calling close
  4. Check that the cleanup has occurred
public void testAutoClose() {
  char[] origChars = "myPrivateData".toCharArray();
  char[] implChars;
  try (SensitiveData data = new SensitiveData(origChars)) {
    // Save the sensitiveData char array
    implChars = (char[]) getField(SensitiveData.class,
                     "sensitiveData", data);
  }
  // After close, was it cleared?
  char[] zeroChars = new char[implChars.length];
  assertEquals(implChars, zeroChars,
               "SensitiveData chars not zero: ");
}

Fairly straight-forward, the getField utility method below is used to get the private char array to be cleared from SensitiveData.sensitiveData.

import java.lang.reflect.Field;

/**
 * Get an object from a named field.
 */
static Object getField(Class<?> clazz, 
  String fieldName, Object instance) {
  try {
    Field field = clazz.getDeclaredField(fieldName);
    field.setAccessible(true);
    return field.get(instance);
  } catch (NoSuchFieldException | IllegalAccessException ex) {
    throw new RuntimeException("field unknown or not accessible");
  }
}


Testing the Cleaner of SensitiveData

Verifying the cleanup in the un-reachable case is a little more interesting. The cleanup function won’t be run until sometime after the garbage collector determines the object is un-reachable. The setup is the same, as is checking that the array has been cleared. The try-with-resources is replaced with clearing the reference to the SensitiveData so it can be garbage collected.

  1. Create and hold a reference to the object being cleaned
  2. Extract a reference to the encapsulated data that is to be cleaned
  3. Drop the reference to the object
  4. Request the garbage collector to run
  5. Poll the array, checking for the cleanup to be complete
public void testCharArray() {
  final char[] origChars = "myPrivateData".toCharArray();
  SensitiveData data = new SensitiveData(origChars);

  // A reference to sensitiveData char array
  char[] implChars = (char[]) getField(SensitiveData.class, 
                       "sensitiveData", data);

  data = null;  // Remove reference

  char[] zeroChars = new char[implChars.length];
  for (int i = 10; i > 0; i--) {
    System.gc();
    try {
      Thread.sleep(10L);
    } catch (InterruptedException ie) { }

    if (Arrays.equals(implChars, zeroChars))
      break;    // break as soon as cleared
  }
  // Check and report any errors
  assertEquals(implChars, zeroChars, 
               "After GC, chars not zero");
}

As in the AutoCloseable case above, the internal sensitiveData char array is saved by the test. After the reference to the SensitiveData object is set to null, System.gc() is used to start the garbage collector before checking the array for zeros. The garbage collector runs in parallel, and it may take some time after invoking System.gc() before the garbage collector determines the object is no longer referenced and the Cleaner is notified to call Cleanable.clean() invoking the cleanup function.

This test code directly confirms the array has been cleared. As long as the state that is to be cleaned is visible to the test it works well, but for other cases and classes being tested the state may not always be visible or accessible.


An Alternative Test for Off-heap Resources

For some kinds of resource cleanup, it is not possible for the test to directly observe that the cleanup has or has not occurred. For example, if the resource is a native memory address or a handle, the cleanup function may deal directly with the resource, and it may not be possible for the test to observe that the resource is released or cleared.

The next best thing is to observe that the cleanup function has been called and is complete. To see how that works, we’ll need to understand a bit about how the Cleaner determines when to call the cleanup function.

A Cleaner is a thread that waits for notification that a registered object is no longer reachable and then calls the corresponding cleanup function. When a cleanup function is registered with the Cleaner, the object and its cleanup function are linked by creating and returning a Cleanable. Typically, the Cleanable object is saved in the object, as is done in the SensitiveData example, so that the close method can call Cleanable.clean to invoke the cleaning function and remove the registration.

The Cleanable is implemented as a PhantomReference to the object. The PhantomReference will not keep the object alive and can be queried to know if the object is still alive. During the normal garbage collection process, when the object becomes unreachable, the Cleanable is queued for processing by the Cleaner thread. Until it has been cleaned, the Cleanable is referenced by the Cleaner and won’t be garbage collected. After its cleaning function has been called, the Cleanable itself is freed and garbage collected.

Using the same reference based techniques used to trigger the cleanup, the test can monitor the Cleanable and know it is complete when the Cleanable becomes un-referenced. The test retrieves the Cleanable from the SensitiveData.cleanable field and creates its own PhantomReference to monitor it using its own ReferenceQueue polling utility.

  1. Create and hold a reference to the object being cleaned
  2. Extract a reference to the Cleanable that holds the cleanup function.
  3. Check that the cleanup does not occur before dropping the reference to the object
  4. Drop the reference to the object
  5. Wait for the Cleanable to be no longer referenced
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
  
public void testCleanable() {
    final char[] origChars = "myPrivateData".toCharArray();
    SensitiveData data = new SensitiveData(origChars);

    // Extract a reference to the Cleanable
    Cleanable cleanable = (Cleaner.Cleanable)
             getField(SensitiveData.class, "cleanable", data);

    ReferenceQueue<Object> queue = new ReferenceQueue<>();
    PhantomReference<Object> ref = 
             new PhantomReference<>(cleanable, queue);
    cleanable = null;   
    // Only the Cleaner will have a strong 
    // reference to the Cleanable

    // Check that the cleanup does not happen 
    // before the reference is cleared.
    assertNull(waitForReference(queue), 
               "SensitiveData cleaned prematurely");

    data = null;    // Remove the reference 

    assertEquals(waitForReference(queue), ref,
                 "After GC, not cleaned");
}

This test uses a utility method waitForReference to invoke garbage collection and wait for a reference to be queued. The caller checks if it is the expected PhantomReference to the object.

/**
 * Wait for a Reference to be enqueued.
 * Returns null if no reference is queued within 0.1 seconds
 */
static Reference<?> waitForReference(ReferenceQueue<Object> queue) {
  Objects.requireNonNull(queue);
  for (int i = 10; i > 0; i--) {
    System.gc();
    try {
      var r = queue.remove(10L);
      if (r != null) {
        return r;
      }
    } catch (InterruptedException ie) {
      // ignore, the loop will try again
    }
  };
  return null;
}

This technique for testing cleanup functions relies on knowledge of the implementation of the SensitiveData class and its use of Cleanable objects managed by the Cleaner.

Testing the initial setup of the Cleanable, before the object reference is set to null, verifies that the cleanup is not called prematurely, if so, that’s likely a bug in the test or the implementation.

This technique, waiting for the Cleanable to be called and become un-reachable, is effective independent of whether the cleanup function is chosen to be a simple lambda or an explicit record class, nested class, or top level class. Though it does not directly verify the cleanup, it does verify that the cleanup function has been called and completed.

These are only a few possible ways to write the test, there are many more that take advantage of cooperation with the class being tested. By refactoring the state or allowing the test to break the encapsulation of the class both the cleanup function and the test can have the visibility needed to confirm the cleanup occurs and occurs when expected.

The SensitiveData example code and the SensistiveDataTest test code are available in a SensitiveData Gist. The tests use `TestNG for test assertions.

~

Originaly posted here.